# 4G USB共享网卡 ***Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.*** --- 本方案基于UniRTOS构建,实现4G共享上网与本地Portal管理。 USBNET Demo基于UniRTOS运行,结合网络附着、PDP配置、Datacall建立、USBNET共享、子网配置与NAT转发机制,在移远通信模块上完成USB虚拟网卡共享上网、本地页面管理与运行状态展示。 系统支持在模块侧建立蜂窝数据链路,并在数据链路建立完成后启动USBNET共享,使Windows、Linux、Android终端能将设备识别为虚拟网卡并具备上网能力;同时支持在本地网段开放Portal服务,通过浏览器访问管理页面,查看设备信息、网络状态、上下行速率以及今日/本周/本月流量统计,并可执行连接、断开、重启设备、切换协议等控制操作。 ```{image} images/image_RUgobtsVdoKvuLxYxDbc0SmInmf.webp :width: 1912px :height: 948px :align: center ``` # 开发资源 | **资源名称** | **数量** | **简要描述** | **获取方法** | | --- | --- | --- | --- | | pico开发板 | 1 | 项目运行的硬件平台 | [点此购买]() | | USB数据线 | 1 | 用于连接PC和开发板,非“仅充电”类型USB-A转USB-C。 | [点此购买]() | | PC | 1 | 系统为Windows 10/Windows 11。 | 自行准备 | | SIM卡 | 1 | 可正常上网的USIM卡。 | 自行准备 | # 快速上手 ## 硬件连接 ```{image} images/image_YMAibOrLnoVHm1xKN2QcWFL1n6d.webp :width: 404px :height: 704px :align: center ``` 1. 将SIM卡插入到开发板SIM卡槽内。 2. 使用数据线连接开发板和电脑。 ## 软件部署 ### 开发环境搭建 参见 [快速上手](),搭建开发环境并完成基本开发流程。 ### 代码拉取 打开一个新的PowerShell窗口,执行以下命令: ```plaintext # 拉取示例仓库 unirtos-cli new -r unirtos-maker-examples # 进入该项目 cd unirtos-maker-examples/4g_donlg ``` ### 修改配置参数 在打开的PowerShell窗口继续执行环境配置命令: ```powershell unirtos-cli env-setup ``` 在PowerShell窗口执行固件编译命令(若模块型号非EG800ZCN_LA,请替换为实际型号): ```powershell unirtos-cli build -m EG800ZCN_LA -v EG800ZCN_LA_BETA_20260904 ``` 等待编译结束后,编译输出末尾会提示固件编译结果: ```plaintext SUCCESS: Unirtos project built successfully! ``` 编译成功后,使用QFlash将固件烧录到开发板。 ### 板上验证 1. 长按开发板电源键(PWKY)开机,打开EPAT工具,根据 [日志调试]() 进行配置,配置完成后重启开发板,从模块上电开始抓取模块日志,确保日志输出完整。 ```{image} images/image_W51jbVCRpomm31xHAMxci2wMnff.webp :width: 1280px :height: 701px :align: center ``` 2. 在网络适配器中确认是否出现Remote NDIS based Internet Sharing Device设备。 ```{image} images/image_On7fbTJ1Hom9srxFTPRcs5qqnlg.webp :width: 307px :height: 27px :align: center ``` 3. 打开浏览器访问192.168.6.1,能打开内置页面即表示联网成功。 ```{image} images/image_ISXzbmOTdocCkhxwZpbck2RWnjg.webp :width: 1912px :height: 948px :align: center ``` # 软件设计详解 ## 方案框架 ```{image} images/image_HpISbx6z7ogZKoxk2dbcrl7hnNc.webp :width: 951px :height: 673px :align: center ``` ## 业务流程 ```{figure} images/board_YPvpwR8lZhEXaHbxWaCcbS1rn4c.jpg :align: center :alt: image ``` ## 启动流程 ```{figure} images/board_XtJWwB4BJhJhXtbNskic786En3f.jpg :align: center :alt: image ``` **启动流程说明** 1. 系统启动后,应用入口函数 *unir_usbnet_demo_init()* 通过 *UNIRTOS_APP_EXPORT()* 自动注册并执行。 2. *unir_usbnet_demo_init()* 负责检查主任务是否已存在;若不存在,则创建 *qosa_usbnet_demo_task()* 主任务。 3. *qosa_usbnet_demo_task()* 启动后,先初始化运行参数,读取当前USBNET类型,并在类型不符合预期时执行 *qosa_usbnet_set_type()* 和设备复位。 4. 当USBNET类型满足要求后,主任务继续等待网络附着,配置PDP Context,设置USB子网,启用NAT,建立Datacall,并启动USBNET共享链路。 5. USBNET启动成功后,程序初始化Portal状态缓存,并依次创建状态采集任务 *qosa_usbnet_status_task()* 和Portal服务任务 *qosa_usbnet_portal_task()*;若已启用应用层DNS方案,仍会继续创建DNS任务。 6. 主任务完成初始化后进入监督循环,持续处理待执行动作、执行策略检查,并维持服务运行;由此,Portal任务既能响应页面控制请求,也不必直接承担底层链路的控制压力。 ## 代码讲解 ### 项目结构 ```python usbnet/ ├── CMakeLists.txt # 工程构建入口 ├── env_config.json # 构建环境配置 ├── usb_net.c # USBNET Demo核心业务实现 ├── menuconfig/ # 配置目录 ├── qos_build/ # 构建输出目录 ├── release/ # 打包产物目录 └── tools/ # 辅助工具目录 ``` ### 入口初始化 *unir_usbnet_demo_init()*: 1. 打印启动日志。 2. 检查USBNET Demo主任务是否已存在。 3. 调用 *qosa_task_create()* 创建 *qosa_usbnet_demo_task()*。 ```c void unir_usbnet_demo_init(void) { int err = 0; ### 主任务与建链逻辑 if (g_usbnet_demo_task_ptr == QOSA_NULL) { QLOGI("create usbnet demo task"); err = qosa_task_create(&g_usbnet_demo_task_ptr, 4 * 1024, QOSA_PRIORITY_NORMAL, "usbnet_demo", qosa_usbnet_demo_task, QOSA_NULL); if (err != QOSA_OK) { QLOGI("USBNET demo init task create error"); return; } QLOGI("create usbnet demo task ok"); } return; } UNIRTOS_APP_EXPORT(200, "usb_net", unir_usbnet_demo_init); ``` 上述代码通过UniRTOS的应用注册宏 *UNIRTOS_APP_EXPORT()*,将USBNET Demo的入口函数注册到系统应用初始化列表中,由框架统一调度。 ### 主控制与建链逻辑 USBNET Demo的主控制逻辑位于 *usb_net.c* 的 *qosa_usbnet_demo_task()*,链路建立的入口位于 *usb_net.c* 的 *_usbnet_demo_connect_data()*。 ```c static qosa_bool_t _usbnet_demo_connect_data(void) { qosa_int32_t ret = 0; qosa_datacall_conn_t conn = {0}; qosa_usbnet_config_t *config = QOSA_NULL; conn = qosa_datacall_conn_new(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx, QOSA_DATACALL_CONN_UNDEFINED); if (!qosa_datacall_get_status(conn)) { _usbnet_demo_configure_pdp_context(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx); ret = qosa_datacall_start(conn, USBNET_DEMO_WAIT_ATTACH_MAX_WAIT_TIME); if (ret != QOSA_DATACALL_OK) { QLOGI("portal connect datacall fail, ret=%d", ret); return QOSA_FALSE; } } if (!_usbnet_demo_apply_usbnet_config(g_usbnet_demo_method)) { QLOGI("portal apply usbnet config fail"); return QOSA_FALSE; } _usbnet_demo_configure_usbnet_subnet(g_usbnet_demo_portal_simid); _usbnet_demo_log_usbnet_subnet(g_usbnet_demo_portal_simid); _usbnet_demo_enable_nat(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx); _usbnet_demo_cache_upstream_dns(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx); config = (qosa_usbnet_config_t *)qosa_malloc(sizeof(qosa_usbnet_config_t)); if (config == QOSA_NULL) { return QOSA_FALSE; } config->simid = g_usbnet_demo_portal_simid; config->pdpid = g_usbnet_demo_portal_profile_idx; config->method = g_usbnet_demo_method; ret = qosa_usbnet_start(g_usbnet_demo_portal_simid, _usbnet_demo_func_cb, config); if (ret != QOSA_USBNET_ERR_OK) { QLOGI("portal usbnet start fail, ret=%d", ret); qosa_free(config); return QOSA_FALSE; } return QOSA_TRUE; } ``` 上述代码体现了USBNET Demo的主链路策略:先建立蜂窝数据出口,再配置USB本地子网并启用NAT,最后启动USBNET共享。Portal页面与状态服务依赖底层链路的可用性。 ### 状态模型与缓存 页面展示依赖统一状态对象 *usbnet_demo_portal_status_t*。该结构定义位于 *usb_net.c*,主要聚合以下几类信息: - 设备信息:型号、固件版本。 - 网络信息:网络状态、运营商、网络制式、USBNET状态。 - 流量信息:上下行速率、今日/本周/本月统计。 - 控制信息:自动重连、策略状态、活动设备、限制额度。 - 原始统计字段:总流量、上下行字节、时间有效标记。 Portal页面所需的动态数据,统一由 *usb_net.c* 的 *_usbnet_demo_collect_portal_status()* 和 *usb_net.c* 的 *_usbnet_demo_collect_portal_status_light()* 采集,再通过缓存接口集中保存。 ```c static void _usbnet_demo_collect_portal_status(usbnet_demo_portal_status_t *status) { qosa_int32_t len = 0; qosa_uint8_t ps_status = QOSA_NW_REG_UNKNOWN; qosa_datacall_conn_t conn = {0}; qosa_datacall_traffic_statistics_t traffic_statistics = {0}; qosa_nw_oper_name_t short_name = {0}; qosa_usbnet_status_e usbnet_status = QOSA_USBNET_STATUS_IDLE; if (status == QOSA_NULL) { return; } _usbnet_demo_init_portal_status_defaults(status); /* 后续依次采集型号、版本、运营商、注册状态、网络制式、USBNET状态、Datacall状态和流量统计 */ } ``` *usb_net.c* 中的状态采集任务 *qosa_usbnet_status_task()* 采用“重采集+轻采集”双路径模型:低频周期执行全量状态更新,高频周期只刷新时间、流量这类热点字段。由此,既能保持页面近实时刷新,又能降低对底层接口的查询压力。 对应的缓存读写函数位于 *usb_net.c*。这种“先采集、再缓存、后输出”的组织方式,使页面渲染和状态采样解耦,避免多个HTTP请求同时直接访问底层接口。 ### Portal页面与状态接口 Portal服务主入口位于 *usb_net.c* 的 *qosa_usbnet_portal_task()*,请求处理主逻辑位于 *usb_net.c* 的 *_usbnet_demo_handle_portal_client()*。 ```c static void qosa_usbnet_portal_task(void *arg) { qosa_int32_t portal_fd = -1; qosa_int32_t captive_fd = -1; qosa_int32_t client_fd = -1; QOSA_UNUSED(arg); QLOGI("usbnet portal task start"); while (g_usbnet_demo_service_running) { portal_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_PORTAL_PORT); captive_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_CAPTIVE_PORT); /* 通过select/accept接收客户端请求,并调用_usbnet_demo_handle_portal_client()处理 */ } } ``` Portal服务承担两类输出: - 页面输出:返回Portal管理页HTML。 - 数据输出:返回 */status* JSON。 其中 */status* JSON的生成由 *usb_net.c* 的 *_usbnet_demo_build_status_json()* 完成,页面中的网络状态、上下行速率、今日/本周/本月流量、策略状态和通知信息均来自该函数。 页面并非独立前端工程,而是由C代码在运行时拼接HTML、CSS和JavaScript后直接返回浏览器;这种方式部署简单、资源集中,但模板较长、维护成本偏高。为此,代码按功能将模板拆分为多段拼接,以降低超长格式串带来的风险。 ### 控制动作与监督循环 Portal页面支持连接、断开、重启、协议切换等控制动作,但这些动作不会直接在HTTP请求处理中执行,而是先入队,再由主任务统一处理。 ```c static void _usbnet_demo_process_pending_action(void) { usbnet_demo_portal_action_e action = g_usbnet_demo_pending_action; if (action == USBNET_DEMO_PORTAL_ACTION_NONE) { return; } g_usbnet_demo_pending_action = USBNET_DEMO_PORTAL_ACTION_NONE; g_usbnet_demo_action_busy = QOSA_TRUE; switch (action) { case USBNET_DEMO_PORTAL_ACTION_CONNECT: _usbnet_demo_set_notice(_usbnet_demo_connect_data() ? "Connect request executed." : "Connect request failed."); break; case USBNET_DEMO_PORTAL_ACTION_DISCONNECT: _usbnet_demo_set_notice("Disconnect request executing."); _usbnet_demo_disconnect_data(); break; case USBNET_DEMO_PORTAL_ACTION_REBOOT: _usbnet_demo_set_notice("Reboot request executing."); qosa_power_reset(QOSA_RESET_NORMAL); break; default: break; } g_usbnet_demo_action_busy = QOSA_FALSE; } ``` 这种设计体现了典型的嵌入式控制原则:Portal任务只负责接收请求,耗时动作由主任务统一串行执行,从而降低HTTP请求与底层链路控制并发执行的风险。 ### 网络限制与策略设计 网络限制能力由 *usbnet_demo_access_policy_t* 和 *_usbnet_demo_apply_policy_request()* 组成,主要支持以下配置: - 是否允许网络访问。 - 是否启用时间计划。 - 起止时间窗口。 - 总流量限制。 - Windows / Linux设备单独限制。 ```c static void _usbnet_demo_apply_policy_request(const char *request, char *message, qosa_uint32_t message_len) { usbnet_demo_access_policy_t next_policy = g_usbnet_demo_policy; char value[24] = {0}; if (_usbnet_demo_extract_query_value(request, "network_enabled", value, sizeof(value))) { next_policy.network_enabled = _usbnet_demo_parse_bool_value(value, next_policy.network_enabled); } /* 后续继续解析schedule、limit、device policy等字段 */ g_usbnet_demo_policy = next_policy; _usbnet_demo_set_text(message, message_len, "Network policy saved."); } ``` 当前实现中,策略保存、页面展示和状态回显均已实现,但 *usb_net.c* 的 *_usbnet_demo_enforce_policy()* 仍采用保守实现,没有真正执行硬阻断。这意味着当前版本定位为“管理配置+状态展示”的Demo,而不是完整的接入控制系统。 ### 异步回调与资源回收 USBNET异步回调位于 *usb_net.c* 的 *_usbnet_demo_func_cb()*。 ```c static void _usbnet_demo_func_cb(void *ctx, void *argv) { qosa_usbnet_config_t *usbnet_config = (qosa_usbnet_config_t *)ctx; qosa_usbnet_general_cnf_t *cnf = argv; QLOGI("usbnet callback enter"); if ((cnf != QOSA_NULL) && (QOSA_OK == cnf->err_code)) { QLOGI("usbnet callback success"); } else { QLOGI("usbnet callback failed"); } qosa_free(usbnet_config); } ``` 该回调不负责业务决策,主要职责是接收SDK异步结果、打印日志并回收 *qosa_usbnet_config_t*。从职责划分上看,可视为USBNET控制链路的完成通知与资源释放点。 # 应用演示 为了更直观地体现USBNET Demo的跨平台适配能力,演示场景分别针对Windows PC、Linux(Ubuntu)和Android三类终端展开。三类终端均通过USB接入模块侧USBNET共享网络,在终端侧获取本地网段地址后访问Portal页面,展示“可识别、可上网、可管理、可控制”的完整闭环。 ## Windows PC平台演示效果 1. PC识别USB虚拟网卡,在设备管理器-网络适配器中能够看到NDIS,即表示识别成功。 ```{image} images/image_CA4hbOGupo9aDPxpVDAcypLynQh.webp :width: 345px :height: 65px :align: center ``` 2. 打开 **cmd** 并执行 **ipconfig**,确认已获取基本网络信息。此时关闭其余网络接口,仅保留USB网卡的网络接口,电脑仍可正常上网。 ```{image} images/image_GJYFbUV1Yo0k1Gx4Dv1cRpG6nLc.webp :width: 623px :height: 142px :align: center ``` 3. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息,即表示可正常访问网络。 ```{image} images/image_X5F1bBaVWoJRhUxE2DgcTO7HnuI.webp :width: 1912px :height: 948px :align: center ``` ## Linux(Ubuntu)平台演示效果 在Linux(Ubuntu)环境下接入模块后,需要按下按键切换ECM协议模式。 1. 将设备的USB接口接入Linux系统。 2. 网卡加载后,通过 **ifconfig** 命令查看网卡状态,确认新增usb1网卡。如下图所示: ```{image} images/image_PAIMbrhEroMGOFxD8wfcTS1bnef.webp :width: 663px :height: 489px :align: center ``` 3. 确认网络连接。 通过 **ping** www.baidu.com确认网络连通性。如下图所示: ```{image} images/image_P0u5bMAofo1Cv2xqnUzc7KIznoh.webp :width: 559px :height: 209px :align: center ``` 4. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。 ```{image} images/image_BE6vb0iBwolLcuxtOJyc8O01nWg.webp :width: 1912px :height: 948px :align: center ``` ## Android平台演示效果 在Android终端环境下,可通过支持OTG的手机连接USB网卡。 1. Android终端接入设备后,需要打开OTG模式。 2. 模块连接手机并上电后,手机状态栏会显示<···>图标,表示已识别到USB网卡,此时可通过浏览器正常访问网络。 ```{image} images/image_LKj9bdMbdoQSABxKyBcc17tOnqb.webp :width: 370px :height: 75px :align: center ``` 3. 浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。 ```{image} images/image_MghhbPW9zoA19cxztCYcs6T2nec.webp :width: 299px :height: 559px :align: center ```